package cn.tlb.upload;

import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import android.util.Log;

import java.io.BufferedReader;
import java.io.Closeable;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.lang.ref.WeakReference;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;

/**
 * Created by 汤林冰 on 2015/12/25 15:31.
 * <p><li style="color:red;font-weight:bold;font-size: 18px">注意: <code>FormUtil</code> 必须在主线程创建</li></p>
 * <pre>
        // 注意: 必须在主线程创建
        FormUtil formUtil = new FormUtil();
        //创建表单对象 &lt;form action="http://www.baidu.com" method="POST"&gt;&lt;input>&lt;/form&gt;
        Form form = new Form("http://192.168.50.211:8135/test/uploadfile.php", "POST");
        //往表单中添加输入项 &lt;input type="file", name="file1", value="ROOT.getAbsolutePath() + "/cn.tlb/DecorView.java""&gt;
        form.inputs.add(Input.newTextInput("name1", "张飞关羽"));//普通字段
        form.inputs.add(Input.newFileInput("resfile", ROOT.getAbsolutePath() + "/Movies/1.mp4"));//文件

        //表单提交
        formUtil.uploadMultipartForm(form, new FormUtil.OnSubmitListener() {
             @Override
             public void onStart() {
                 progressDialog = new ProgressDialog(UploadActivity.this);
                 progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
                 progressDialog.show();
             }

             @Override
             public void onError(Throwable th) {
                Toast.makeText(UploadActivity.this, th.getMessage(), Toast.LENGTH_SHORT).show();
             }

             @Override
             public void onUploading(String absFilePath, long totalSize, long uploadSize, int progress) {
                 Log.i(TAG, String.format("onUploading(absFilePath=%s, totalSize=%d, long uploadSize=%d, int progress=%d",
                 absFilePath, totalSize, uploadSize, progress));
                 progressDialog.setProgress(progress);
             }

             @Override
             public void onCancel() {
                Toast.makeText(UploadActivity.this, "onCancel", Toast.LENGTH_SHORT).show();
             }

             @Override
             public void onSuccess(String result) {
                 Toast.makeText(UploadActivity.this, result, Toast.LENGTH_SHORT).show();
                 Log.i(TAG, "onSuccess(" + result + ")");
             }

             @Override
             public void onFailed(int responseCode) {
                Toast.makeText(UploadActivity.this, "responseCode;" + responseCode, Toast.LENGTH_SHORT).show();
             }

             @Override
             public void onFinish() {
                progressDialog.dismiss();
             }
        });
  </pre>
 */
public class FormUtil {
    private static final String     TAG                         = FormUtil.class.getSimpleName();
    private static final String     BOUNDARY                    = "---------------------------7df1c314e08f2";
    private static final String     TWO_DASH                    = "--";
    private static final String     LINE_SEPARATOR              = "\r\n";
    /**默认编码集*/
    private static final String     UTF_8                       = "utf-8";

    private static final int        START                       = 0x0;
    private static final int        SUCCESS                     = 0x1;
    private static final int        FAILED                      = 0x2;
    private static final int        ERROR                       = 0x4;
    private static final int        UPLOADING                   = 0x8;
    private static final int        CANCEL                      = 0x10;
    private static final int        FINISH                      = 0x20;

    private static final long       UPDATE_PROGRESS_FREQUENCY   = 500/*毫秒*/;
    /**上传文件的绝对路径*/
    private static final String     KEY_FILE_PATH               = "FILE_PATH";
    /**long类型数据key: 上传文件的大小*/
    private static final String     KEY_TOTAL_SIZE              = "TOTAL_SIZE";
    /**long类型数据key: 已上传的字节数*/
    private static final String     KEY_UPLOADING_SIZE          = "UPLOADING_SIZE";

    private boolean                 uploading;
    private OnSubmitListener        listener;
    private Handler                 handler;

    /**
     * 文件上传回调接口, 所有方法可直接在主线程调用
     */
    public interface OnSubmitListener{
        /**
         * 准备上传
         */
        void onStart();

        /**
         * 提交表单中发生的错误:服务器URL错误、文件类型不支持(Content-Type)、IO读写错误等
         * @param th
         */
        void onError(Throwable th);

        /**
         * 上传信息(只有类型是文件才有进度)
         * @param absFilePath 当前上传的文件绝对路径
         * @param totalSize   文件总大小(字节)
         * @param uploadSize  已上传的字节个数
         * @param progress    已上传的进度
         */
        void onUploading(String absFilePath, long totalSize, long uploadSize, int progress);

        /**
         * 取消上传, 通过 {@link #cancel()}触发回调
         */
        void onCancel();

        /**
         *
         * @param result 表单提交成功时,服务器的响应结果
         */
        void onSuccess(String result);

        /**
         * 连接服务器响应码 != 200
         * @param responseCode 服务器响应码
         */
        void onFailed(int responseCode);

        /**
         * 请求完成 {@link #cancel()},{@link #onError(Throwable)},{@link #onSuccess(String)},
         * 或{@link #onFailed(int)}之后执行
         */
        void onFinish();
    }

    /**
     * 上传 multipart/form-data 表单数据(单线程)
     * @param form
     * @param listener
     */
    public void uploadMultipartForm(Form form, OnSubmitListener listener){
        this.listener = listener;
        handler = new UploadHandler(new WeakReference<FormUtil>(this));
        new Thread(new MultiPartRunnable(form)).start();
    }

    /**
     * 取消上传
     */
    public void cancel(){
        Message.obtain(handler, CANCEL).sendToTarget();
    }

    /**
     * 是否在上传中
     * @return
     */
    public boolean isUploading() {
        return uploading;
    }

    public static String getFileName(String filepath){
        int start = filepath.lastIndexOf("/");
        return filepath.substring(start+1);
    }

    /**
     <p>This Handler class should be static or leaks might occur (null)</p>
     <p>Since this Handler is declared as an inner class, it may prevent the outer class from being
     garbage collected. If the Handler is using a Looper or MessageQueue for a thread other than
     the main thread, then there is no issue. If the Handler is using the Looper or MessageQueue
     of the main thread, you need to fix your Handler declaration, as follows: Declare the Handler
     as a static class; In the outer class, instantiate a WeakReference to the outer class and pass
     this object to your Handler when you instantiate the Handler; Make all references to members
     of the outer class using the WeakReference object.</p>
     */
    private static class UploadHandler extends Handler {
        OnSubmitListener listener;
        FormUtil formUtil;

        public UploadHandler(WeakReference<FormUtil> formUtilWeakReference){
            formUtil = formUtilWeakReference.get();
            listener = formUtil.listener;
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case START:
                    listener.onStart();
                    break;
                case ERROR:
                    listener.onError((Throwable) msg.obj);
                    break;
                case UPLOADING:
                    Bundle data = msg.getData();
                    String path = data.getString(KEY_FILE_PATH);
                    long total = data.getLong(KEY_TOTAL_SIZE);
                    long current = data.getLong(KEY_UPLOADING_SIZE);
                    int progress = (int) (current * 100 / total);
                    listener.onUploading(path, total, current, progress);
                    break;
                case CANCEL:
                    formUtil.uploading = false;
                    listener.onCancel();
                    break;
                case SUCCESS:
                    listener.onSuccess((String) msg.obj);
                    break;
                case FAILED:
                    listener.onFailed((Integer) msg.obj);
                    break;
                case FINISH:
                    listener.onFinish();
                    break;
            }
        }
    }

    private class MultiPartRunnable implements Runnable {
        private Form form;

        public MultiPartRunnable(Form form) {
            this.form = form;
        }

        @Override
        public void run() {
            Message.obtain(handler, START).sendToTarget();
            uploading = true;
            DataOutputStream dos = null;
            try {
                URL url = new URL(form.action);
                HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
                urlConnection.setConnectTimeout(15000);
                if (form.method == null) {
                    form.method = "GET";
                }
                if ("POST".equals(form.method.toUpperCase())) {
                    urlConnection.setDoOutput(true);
                    urlConnection.setUseCaches(false);
                    urlConnection.setRequestMethod("POST");
                }

                urlConnection.addRequestProperty("Content-Type", "multipart/form-data; boundary=" + BOUNDARY);
                dos = new DataOutputStream(urlConnection.getOutputStream());
                dos.write(LINE_SEPARATOR.getBytes());

                for (int i = 0; uploading && i < form.inputs.size(); i++) {
                    Input input = form.inputs.get(i);
                    switch (input.type) {
                        case TEXT:
                            dos.write((TWO_DASH + BOUNDARY).getBytes());
                            dos.write(LINE_SEPARATOR.getBytes());
                            dos.write(String.format("Content-Disposition: form-data; name=\"%s\"", input.name).getBytes());
                            dos.write(LINE_SEPARATOR.getBytes());
                            dos.write(LINE_SEPARATOR.getBytes());
                            dos.write(input.value.getBytes(UTF_8));
                            dos.write(LINE_SEPARATOR.getBytes());
                            break;
                        case FILE:
                            dos.write((TWO_DASH + BOUNDARY).getBytes());
                            dos.write(LINE_SEPARATOR.getBytes());
                            dos.write(String.format("Content-Disposition: form-data; name=\"%s\"; filename=\"%s\"",
                                    input.name, getFileName(input.filename)).getBytes());
                            dos.write(LINE_SEPARATOR.getBytes());
                            String contentType = URLConnection.guessContentTypeFromName(input.filename);
                            if (contentType == null) {
                                throw new IllegalArgumentException("unsupported content type:" + contentType);
                            }
                            dos.write(("Content-Type: " + contentType).getBytes());
                            dos.write(LINE_SEPARATOR.getBytes());
                            dos.write(LINE_SEPARATOR.getBytes());

                            File file = new File(input.filename);
                            FileInputStream in = new FileInputStream(file);
                            String filePath = file.getAbsolutePath();
                            long totalSize = file.length();
                            long uploadSize = 0;
                            long time = SystemClock.elapsedRealtime();

                            byte[] bs = new byte[8 * 1024];
                            int len;
                            while (uploading && (len = in.read(bs)) != -1) {
                                dos.write(bs, 0, len);
                                uploadSize += len;

                                //send progress
                                if(uploadSize >= totalSize
                                        || SystemClock.elapsedRealtime() - time >= UPDATE_PROGRESS_FREQUENCY) {
                                    time = SystemClock.elapsedRealtime();
                                    Bundle data = new Bundle();
                                    data.putString(KEY_FILE_PATH, filePath);
                                    data.putLong(KEY_TOTAL_SIZE, totalSize);
                                    data.putLong(KEY_UPLOADING_SIZE, uploadSize);
                                    Message msg = Message.obtain();
                                    msg.what = UPLOADING;
                                    msg.setData(data);
                                    handler.sendMessage(msg);
                                }
                            }
                            close(in);
                            dos.write(LINE_SEPARATOR.getBytes());
                            break;
                    }
                }

                dos.write((TWO_DASH + BOUNDARY + TWO_DASH + LINE_SEPARATOR).getBytes());
                dos.flush();
                dos.close();

                if(uploading) {
                    if (urlConnection.getResponseCode() == HttpURLConnection.HTTP_OK) {
                        BufferedReader br = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
                        StringBuffer sb = new StringBuffer();
                        String line;
                        while ((line = br.readLine()) != null) {
                            sb.append(line).append(LINE_SEPARATOR);
                        }
                        Log.i(TAG, "Response: " + sb);
                        Message.obtain(handler, SUCCESS, sb.toString()).sendToTarget();
                    } else {
                        Message.obtain(handler, FAILED, urlConnection.getResponseCode()).sendToTarget();
                    }
                }
            }catch(Exception e){
                Message.obtain(handler, ERROR, e).sendToTarget();
            }finally {
                close(dos);
                uploading = false;
                Message.obtain(handler, FINISH).sendToTarget();
            }
        }
    }

    private static void close(Closeable closeable){
        if(closeable != null){
            try{
                closeable.close();
            }catch(Exception e){
                e.printStackTrace();
            }
        }
    }
}
